package jvs.peer.xmpp;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

import javax.net.SocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;

import jvs.peer.io.File;
import jvs.peer.io.FileTransfer;
import jvs.peer.io.Header;
import jvs.peer.io.Lifecycle;
import jvs.peer.io.MessageConstant;
import jvs.peer.io.MessageDriver;
import jvs.peer.io.MessageListener;
import jvs.peer.io.Messenger;
import jvs.peer.io.Notifier;
import jvs.peer.io.PeerStatus;
import jvs.peer.io.Session;
import jvs.peer.sec.AccessControl;
import jvs.peer.util.Log;

import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.Roster.SubscriptionMode;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Presence.Mode;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.filetransfer.FileTransferManager;

import android.text.TextUtils;

/**
 * @author qiangli
 * 
 */
public class Jabber implements MessageDriver, Messenger, Notifier, PeerStatus, FileTransfer,
		Lifecycle {
	
	public static final String RESOURCE_HOTWEB = "hotweb";
	
	public static final String SLASH_RESOURCE_HOTWEB = "/" + RESOURCE_HOTWEB;

	public static final String ISHELL_PREFIX = "HI: ";

	private static final int ISHELL_PREFIX_LEN = ISHELL_PREFIX.length();

	private static final String LINE_SEP = ";";
	private static final String FIELD_SEP = ",";

	private static class HotMessage extends Message {
		@Override
		public void setTo(String to) {
			super.setTo(to.indexOf("/") == -1 ? (to + SLASH_RESOURCE_HOTWEB) : to);
		}
	}
	
	private XMPPConnection conn;

	private PacketCollector collector;

	// private XmppFileServer fileServer;

	private XmppConnectHandler sessionHandler;

	private XmppMessageFilter messageFilter;

	private AccessControl accessControl;

	private String host;

	private String service;

	private int port;

	private String username;

	private String nickname;

	private String password;

	private String jid;

	private List<XmppMessageListener> listeners;

	private String ports;

	private boolean available = false;

	private XmppFileTransferListener ftpd;

	private FileTransferManager ftm;

	private XmppFileTransfer ftp;

	ConnectionConfiguration cfg = null;

	private String keystore;

	private boolean debug;

	private boolean secure;

	private XmppWebAdapter adapter;

	private java.io.File cache;

	public Jabber() {
	}

	public void init(Properties attr) throws Exception {
		host = attr.getProperty("xmpp.host");
		service = attr.getProperty("xmpp.service");
		port = parseInt(attr.getProperty("xmpp.port"));
		username = attr.getProperty("xmpp.username");
		password = attr.getProperty("xmpp.password");
		nickname = attr.getProperty("xmpp.nickname");
		jid = attr.getProperty("xmpp.jid");
		
		if (TextUtils.isEmpty(jid) || TextUtils.isEmpty(password)) {
			throw new RuntimeException("ID/password required");
		}
		//defaults
		if (TextUtils.isEmpty(username)) {
			username = defaultUsername(jid);
		}
		if (TextUtils.isEmpty(service)) {
			service = "gmail.com";
		}
		if (TextUtils.isEmpty(host)) {
			host = "talk.google.com";
		}
		
		adapter = (XmppWebAdapter) attr.get("xmpp.adapter");
		cache = (java.io.File) attr.get("xmpp.cache");
		
		keystore = attr.getProperty("xmpp.keystore");
		secure = Boolean.parseBoolean(attr.getProperty("xmpp.secure", "false"));
		//
		ports = getHttpPorts(attr);
		
		//
		debug = attr.getProperty("xmpp.debug", "false")
				.equalsIgnoreCase("true");
		//
		initCfg();
	}

	private int parseInt(String s) {
		final int def = 5222;
		try {
			return Integer.parseInt(s);
		} catch (Exception e) {
			//
		}
		return def;
	}

	private String defaultUsername(String jid) {
		int idx = jid.indexOf("@");
		return jid.substring(0, idx);
	}

	private void initCfg() throws Exception {
		if (service == null || service.trim().length() == 0) {
			cfg = new ConnectionConfiguration(host, port, host);
		} else {
			cfg = new ConnectionConfiguration(host, port, service);
		}

		cfg.setReconnectionAllowed(true);
		cfg.setCompressionEnabled(false); // not supported by gtalk?

		cfg.setSelfSignedCertificateEnabled(true);
		cfg.setSASLAuthenticationEnabled(true);
		cfg.setSecurityMode(ConnectionConfiguration.SecurityMode.enabled);
		cfg.setVerifyChainEnabled(false);
		cfg.setVerifyRootCAEnabled(false);
		cfg.setNotMatchingDomainCheckEnabled(false);

//		String sp = keystore;
//		if (sp != null) {
//			cfg.setTruststorePath(sp);
//			cfg.setTruststoreType("jks");
//			cfg.setSelfSignedCertificateEnabled(true);
//			cfg.setTruststorePassword("changeit");
//			cfg.setSASLAuthenticationEnabled(true);
//			cfg.setSecurityMode(ConnectionConfiguration.SecurityMode.enabled);
//		}

		try {
			if (secure) {
				SocketFactory ssf = getSecureSocketFactory(keystore);
				cfg.setSocketFactory(ssf);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		if (debug) {
			XMPPConnection.DEBUG_ENABLED = true;
			System.setProperty("smack.debuggerClass",
					"org.jivesoftware.smack.debugger.ConsoleDebugger");
			cfg.setDebuggerEnabled(true);
		}
	}

	private static InputStream getKeystore(String file)
			throws FileNotFoundException {
		if (file == null) {
			return Jabber.class
					.getResourceAsStream("/info/niwota/web/keystore/keystore.bks");
		}
		return new FileInputStream(file);
	}

	private static SSLSocketFactory getSecureSocketFactory(String keystore)
			throws Exception {
		InputStream is = null;
		try {
			is = getKeystore(keystore);

			char kspass[] = "hotweb".toCharArray();
			char ctpass[] = "hotweb".toCharArray();
			KeyStore ks = KeyStore.getInstance("BKS");

			ks.load(is, kspass);
			String alg = KeyManagerFactory.getDefaultAlgorithm();
			if (Log.DEBUG) {
				Log.log(Jabber.class, "create Secure Socket alg=" + alg
						+ " keystore: " + keystore);
			}
			KeyManagerFactory kmf = KeyManagerFactory.getInstance(alg);
			kmf.init(ks, ctpass);
			SSLContext sc = SSLContext.getInstance("TLS");
			sc.init(kmf.getKeyManagers(), null, null);
			SSLSocketFactory ssf = sc.getSocketFactory();
			return ssf;
		} finally {
			try {
				is.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private static String getHttpPorts(Properties attr) {
		String port = attr.getProperty("http.port", "10080");
		// String sport = attr.getProperty("http.sport", "10081");
		return port;
	}

	public void disconnect() {
		try {
			conn.disconnect();
		} catch (Exception e) {
			// e.printStackTrace();
		} finally {
			available = false;
			//conn = null;
		}
		
	}

	public void connect() throws Exception {
		conn = new XMPPConnection(cfg);
	
		conn.connect();
		//
		if (Log.DEBUG) {
			Log.log(this, "Connecting to " + conn.getHost() + " nickname: "
					+ nickname + " jid: " + jid + " username: " + username);
		}
		
//		if (nickname != null) {
//			conn.login(username, password, nickname, true);
//		} else {
//			conn.login(username, password, "hotweb", true);
//		}
		
		conn.login(username, password, RESOURCE_HOTWEB, true);
		
		if (Log.DEBUG) {
			Log.log(this, "Logged in as " + conn.getUser());
		}
		conn.getRoster().setSubscriptionMode(SubscriptionMode.accept_all);
		
		//
		sendPresence(Presence.Mode.available); // TODO check real status
		//
		jid = StringUtils.parseBareAddress(conn.getUser());
		if (Log.DEBUG) {
			Log.log(this, "User jid: " + jid + " connected: " + conn.getHost());
		}
	}

	private void sendPresence(Presence.Mode mode) {
		String ipAddresses = XmppUtil.ip();
		String msg = ISHELL_PREFIX + ipAddresses + FIELD_SEP + ports + LINE_SEP;
		Presence p = new Presence(Presence.Type.available, msg, 127, mode);
		p.setProperty("ip", ipAddresses);
		p.setProperty("port", ports);
		if (conn != null) {
			conn.sendPacket(p);
		}
	}

	synchronized private void checkConnection() throws IOException {
		if (conn == null || !conn.isConnected()) {
			throw new IOException("Not connected");
		}
	}

	synchronized private XMPPConnection getConnection() {
		return conn;
	}

	public jvs.peer.io.Message receive() throws IOException {
		checkConnection();
		//
		Message msg = (Message) collector.nextResult();
		jvs.peer.io.Message m = convert(msg);
		if (Log.DEBUG) {
			Log.log(this, "<<<<<<< received. tid: " + msg.getThread()
					+ " seqno: " + m.getSeqno() + " ioscmd: " + m.getIoscmd());
		}
		return m;
	}

	public void send(jvs.peer.io.Message msg) throws IOException {
		checkConnection();
		//
		Message m = convert(msg);
		getConnection().sendPacket(m);
		if (Log.DEBUG) {
			Log.log(this, ">>>>>> sent. tid: " + m.getThread() + " seqno: "
					+ msg.getSeqno() + " ioscmd: " + msg.getIoscmd());
		}
	}

	private jvs.peer.io.Message convert(Message msg) throws IOException {
		jvs.peer.io.Message m = new jvs.peer.io.Message();
		//
		String ioscmd = msg.getProperty(MessageConstant.ioscmd).toString();
		String mode = msg.getProperty(MessageConstant.mode).toString();
		String peer = msg.getFrom();
		String tid = msg.getThread();
		String seqno = msg.getProperty(MessageConstant.seqno).toString();

		//
		Header h = new Header();
		h.setPeer(peer);
		h.setThread(tid);
		h.setMode(Integer.parseInt(mode));
		m.setHeader(h);
		//
		m.setIoscmd(Integer.parseInt(ioscmd));
		m.setSeqno(Integer.parseInt(seqno));

		m.setData(msg.getBody());

		return m;
	}

	private Message convert(jvs.peer.io.Message m) throws IOException {
		Message xm = new HotMessage();

		xm.setType(org.jivesoftware.smack.packet.Message.Type.normal);
		xm.setSubject("");
		//
		Header h = m.getHeader();
		xm.setThread(h.getThread());
		xm.setTo(h.getPeer());
		xm.setProperty(MessageConstant.mode, h.getMode());
		//
		xm.setProperty(MessageConstant.ioscmd, m.getIoscmd());
		xm.setProperty(MessageConstant.seqno, m.getSeqno());

		xm.setBody(m.getData());

		return xm;
	}

	public String getJid() {
		return jid;
	}

	public AccessControl getAccessControl() {
		return accessControl;
	}

	public boolean ping(String peer) {
		// no peer is available if user is not signed on
		if (conn == null || !conn.isConnected()) {
			return false;
		}
		Presence p = conn.getRoster().getPresence(peer);
		String s = p.getStatus();
		if (s == null || s.toLowerCase().indexOf(ISHELL_PREFIX) == -1) {
			return false;
		}
		return true;
	}

	 public Monitor receive(String peer, String path, File file)
	 throws Exception {
		 checkConnection();
		 //
		 Monitor mon = ftp.receive(peer, path, file);
		 //
		 Message msg = new HotMessage();
		 msg.setProperty(MessageConstant.FTP, MessageConstant.FTP);
		 msg.setThread(System.currentTimeMillis() + "");
		 msg.setTo(peer);
		 msg.setType(Message.Type.normal);
		 msg.setBody("GET " + path);
		 
		 sendPacket(msg);
		 //
		 return mon;
	 }

	public Monitor send(String peer, String path, File file) throws Exception {
		checkConnection();
		//
		Message msg = new HotMessage();
		msg.setProperty(MessageConstant.FTP, MessageConstant.FTP);
		msg.setThread(System.currentTimeMillis() + "");
		msg.setTo(peer);
		msg.setType(Message.Type.normal);
		msg.setBody("PUT " + path);
		
		sendPacket(msg);
		//
		Monitor mon = ftp.send(peer, path, file);
		return mon;
	}

	public void invite(String peer) {
		try {
			checkConnection();
			//
			Message msg = new HotMessage();
			msg.setTo(peer);
			msg.setType(Message.Type.chat);
			msg.setBody("");
			
			sendPacket(msg);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void send(String to, String tid, String message) throws Exception {
		checkConnection();
		//
		Message msg = new HotMessage();
		msg.setProperty(MessageConstant.UDP, MessageConstant.UDP);
		msg.setThread(tid);
		msg.setTo(to);
		msg.setType(Message.Type.normal);
		msg.setBody(message);
		
		sendPacket(msg);
	}

	public void register(String peer, String tid, MessageListener ml) {
		XmppMessageListener xml = new XmppMessageListener(peer, tid, ml);
		listeners.add(xml);
		conn.addPacketListener(xml, xml);
	}

	public void unregister(MessageListener ml) {
		for (Iterator<XmppMessageListener> it = listeners.iterator(); it
				.hasNext();) {
			XmppMessageListener xml = it.next();
			if (xml.getListener().equals(ml)) {
				it.remove();
				conn.removePacketListener(xml);
			}
		}
	}

	public void changeStatus(Status status) {
		Mode mode = Presence.Mode.available;
		switch (status) {
		case AVAIL:
			break;
		case BUSY:
			mode = Presence.Mode.dnd;
			break;
		case OFF:
			mode = Presence.Mode.away;
			break;
		}
		sendPresence(mode);
	}

	public Status checkStatus(String peer) {
		Roster r = conn.getRoster();
		Presence p = r.getPresence(peer);
		
		if (!p.isAvailable()) {
			return Status.OFF;
		}
		Mode m = p.getMode();
		
		if (m == null) {
			return Status.AVAIL;
		}
		switch (m) {
		case available:
			return Status.AVAIL;
		case dnd:
			return Status.BUSY;
		case away:
			return Status.OFF;
		case xa:
		}
		return Status.OFF;
	}

	public Session createSession(String peer, Session.Callback cb)
			throws Exception {
		checkConnection();
		//
		XmppSession sess = new XmppSession(conn, peer, cb);
		return sess;
	}

	public void release() {
		listeners = null;
	}

	public void shutdown() throws Exception {
		// try {
		// fileServer.shutdown();
		// fileServer.release();
		// } catch (Exception e) {
		// e.printStackTrace();
		// }
		//
		disconnect();
	}

	public void startup() throws Exception {
		connect();
		// access control
		accessControl = new XmppAccessControl(conn);
		// shell service
		sessionHandler = new XmppConnectHandler(conn, adapter);
		// socket service
		messageFilter = new XmppMessageFilter();
		
		// //socket file server
		// fileServer = new XmppFileServer();
		// fileServer.init(null);
		// fileServer.startup();
		// //
		
		ftpd = new XmppFileTransferListener(conn, adapter, cache);
		//
		listeners = new ArrayList<XmppMessageListener>();
		//
		conn.addPacketListener(sessionHandler, new XmppSessionFilter(
				MessageConstant.request));
		//
		collector = conn.createPacketCollector(messageFilter);

		conn.addPacketListener(ftpd, ftpd);
		//
		ftm = new FileTransferManager(conn);
		ftp = new XmppFileTransfer(ftm);

		available = true;
	}

	public String[] getNetAddress(String peer) {
		try {
			Roster r = conn.getRoster();
			Presence p = r.getPresence(peer);
			Mode m = p.getMode();
			if (p.isAvailable() && (m == null || m == Mode.available)) {
				String s = p.getStatus();
				if (s != null) {
					String[] sa = s.substring(ISHELL_PREFIX_LEN)
							.split(LINE_SEP)[0].split(FIELD_SEP);
					for (int i = 0; i < sa.length; i++) {
						sa[i] = sa[i].trim();
					}
					return sa;
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	public boolean available() {
		return available;
	}

	public void register(PresenceListener pl) {
		if (conn != null) {
			Roster roster = conn.getRoster();
			if (roster != null) {
				roster.addRosterListener(pl);
			}
		}
	}

	public void unregister(PresenceListener pl) {
		if (conn != null) {
			Roster roster = conn.getRoster();
			if (roster != null) {
				roster.removeRosterListener(pl);
			}
		}
	}
	
	private void sendPacket(Packet msg) {
		 conn.sendPacket(msg);
	}
}